<?xml version='1.0' encoding='utf-8'?>
<?xml-stylesheet type="text/xsl" href="/sheet.xsl"?><rss version="2.0"><channel><title>카카오스타일 기술 블로그</title><item><title>1년 동안의 iOS 모듈화 진행기 - 2. Component 모듈</title><link>https://devblog.kakaostyle.com/ko/2025-12-30-1-ios-modularization-journey-2nd/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="col-12 col-lg-9" morss_own_score="2.9408866995073892" morss_score="136.65141301529687"&gt;
&lt;p&gt;안녕하세요! 앱 개발팀의 레이몬드입니다. 2024년에 진행했던 모듈화 여정을 &lt;a href="https://devblog.kakaostyle.com/ko/2025-02-10-1-ios-modularization-journey/"&gt;1년 동안의 iOS 모듈화 진행기&lt;/a&gt;로 공유했었는데요, 이번에는 그 여정의 다음 단계인 &lt;strong&gt;Component 모듈&lt;/strong&gt;에 대해 이야기해보려 합니다.&lt;/p&gt;
&lt;p&gt;지난 글에서는 모놀리식 구조에서 출발해 Core 레이어를 분리하고, 의존성을 정리해 나가는 과정을 다뤘습니다. 이번 글에서는 그 위에 쌓인 다음 단계, 즉 &lt;strong&gt;공통 UI 컴포넌트를 모듈로 분리하고, 스토리북·스냅샷 테스트까지 엮어 “관리 가능한 컴포넌트 체계”를 만든 과정&lt;/strong&gt;을 공유하려고 합니다.&lt;/p&gt;
&lt;p&gt;단순히 모듈을 나누는 것을 넘어서서,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;왜 Component 모듈이 필요했는지,&lt;/li&gt;
&lt;li&gt;어떻게 분리했고,&lt;/li&gt;
&lt;li&gt;서버 드리븐 환경에서의 가시성 문제를 어떻게 줄였는지,&lt;/li&gt;
&lt;li&gt;컴포넌트 변경 시 QA 범위를 어떻게 “눈으로 볼 수 있게” 만들었는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;까지 정리해 보겠습니다.&lt;/p&gt;
&lt;h2&gt;1. 왜 Component 모듈이 필요했을까?&lt;/h2&gt;
&lt;h3&gt;1-1. 피처 모듈 분리의 필수 관문&lt;/h3&gt;
&lt;p&gt;제 개인적인 모듈화의 궁극적인 목표는 &lt;strong&gt;피처 모듈 분리와 데모앱을 통한 생산성 향상&lt;/strong&gt;입니다. 하지만 현실에서는 피처 모듈들이 공통 UI 컴포넌트에 강하게 의존하고 있었습니다.&lt;/p&gt;
&lt;p&gt;지그재그 앱에는 다음과 같은 공통 UI 컴포넌트들이 다수 존재했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상품 카드를 포함한 다양한 형태의 &lt;strong&gt;캐러셀&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;여러 지면에서 재사용되는 &lt;strong&gt;헤더 뷰&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;다양한 타입의 &lt;strong&gt;배너&lt;/strong&gt; 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-12-30-1/01.png"&gt;&lt;/p&gt;
&lt;p&gt;이런 컴포넌트들이 &lt;strong&gt;여러 피처에서 얽혀서 사용&lt;/strong&gt;되고 있었고, 이 상태에서는 피처 모듈을 독립적으로 분리하기가 어려웠습니다. 결과적으로 우리는 다음과 같은 결론에 도달했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;피처 모듈을 온전히 분리하려면, 먼저 공통 UI 컴포넌트들을 독립된 &lt;strong&gt;Component 모듈&lt;/strong&gt;로 분리해야 한다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-12-30-1/03.png"&gt;&lt;/p&gt;
&lt;p&gt;여러 지면에서 동일한 컴포넌트를 공유하고 있기 때문에, 이 레이어를 먼저 정리하지 않으면 피처 단에서의 모듈 분리는 항상 어딘가 걸려버리곤 했습니다.&lt;/p&gt;
&lt;h3&gt;1-2. 서버 드리븐 환경에서의 가시성 부족&lt;/h3&gt;
&lt;p&gt;지그재그 앱의 주요 지면은 서버 드리븐 방식으로 운영되고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서버에서 내려주는 데이터에 따라 &lt;strong&gt;어떤 컴포넌트가 어떤 순서로 배치될지&lt;/strong&gt;가 결정되고&lt;/li&gt;
&lt;li&gt;클라이언트는 그 스펙에 맞춰 UI를 그립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 구조는 유연성을 주는 동시에, 다음과 같은 한계를 만들었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;앱에 &lt;strong&gt;어떤 컴포넌트들이 존재하는지 한눈에 볼 수 있는 체계가 없음&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;“이거 예전에 비슷한 거 본 것 같은데…” 싶은데, 실제로 있는지 확인하려면
&lt;ul&gt;
&lt;li&gt;특정 사람에게 물어보거나,&lt;/li&gt;
&lt;li&gt;서버 스펙과 앱 코드를 함께 뒤져봐야 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이로 인해 실제로 이런 문제가 반복해서 발생했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미 유사한 컴포넌트가 존재했지만, 그 존재를 몰라서 &lt;strong&gt;거의 동일한 컴포넌트가 중복 생성&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;“이 컴포넌트 재사용하면 되지 않나요?”를 아는 사람이 일부에 한정되어 있어, 확인만으로도 &lt;strong&gt;큰 커뮤니케이션 비용&lt;/strong&gt;이 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-12-30-1/04.png"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;결과적으로, 서버 드리븐 구조 위에서 &lt;strong&gt;컴포넌트 카탈로그/스토리북의 부재&lt;/strong&gt;가 비용으로 드러나고 있었습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3&gt;1-3. 불명확한 QA 범위&lt;/h3&gt;
&lt;p&gt;서버 드리븐 컴포넌트들은 내부적으로 &lt;strong&gt;공통 View나 공통 헤더·캐러셀&lt;/strong&gt;을 공유하는 경우가 많아서 다음과 같은 상황도 자주 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-12-30-1/05.png"&gt;&lt;/p&gt;
&lt;p&gt;문제는 크게 세 가지였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;특정 컴포넌트를 수정했을 때 &lt;strong&gt;다른 컴포넌트까지 영향을 받는 경우&lt;/strong&gt;가 잦음&lt;/li&gt;
&lt;li&gt;어떤 컴포넌트까지 영향이 가는지, 그리고 그 컴포넌트가 &lt;strong&gt;어떤 지면에서 노출되는지&lt;/strong&gt;를 한눈에 파악하기 어려움&lt;/li&gt;
&lt;li&gt;결과적으로 QA 범위를 명확하게 정의하기 힘들어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;“수정은 했는데, 정확히 어디까지 검증해야 하는가?”가 항상 남는 질문이었습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;2. Component 모듈을 어떻게 분리했나&lt;/h2&gt;
&lt;h3&gt;2-1. 기본 구조: UICollectionViewCell + CellViewModel&lt;/h3&gt;
&lt;p&gt;지그재그 앱의 서버 드리븐 컴포넌트들은 대부분 다음과 같은 구조를 가지고 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-12-30-1/06.png"&gt;&lt;/p&gt;
&lt;h3&gt;2-2. Core 레이어 선행 분리의 효과&lt;/h3&gt;
&lt;p&gt;여기서 큰 도움이 되었던 것은, 이미 &lt;strong&gt;ZCore 모듈로 모델·Repository를 분리해 둔 상태&lt;/strong&gt;였다는 점입니다.&lt;/p&gt;
&lt;p&gt;과거 모듈화 여정에서 우리는 다음을 먼저 진행했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공용 도메인 모델, Repository, LogService 등을 Core 레이어(ZCore)로 분리&lt;/li&gt;
&lt;li&gt;이로 인해 컴포넌트 레이어에서 참조하는 의존성 상당수가 이미 &lt;strong&gt;모듈화된 상태&lt;/strong&gt;였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 Component 모듈 분리는 크게 아래와 같은 작업으로 정리할 수 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;관련 파일들을 ZComponent 모듈로 &lt;strong&gt;이동&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;필요한 타입에 &lt;code&gt;public&lt;/code&gt; 접근 제어자 추가&lt;/li&gt;
&lt;li&gt;사용하는 쪽에 &lt;code&gt;import ZComponent&lt;/code&gt; 추가&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;물론 실제로는 파일 수가 많아 수고가 적지는 않았지만, “Core 분리가 선행된 상태” 덕분에 &lt;strong&gt;의존성 정리에 대한 불확실성&lt;/strong&gt;은 많이 줄어든 상태에서 작업을 시작할 수 있었습니다.&lt;/p&gt;
&lt;h3&gt;2-3. 커밋 전략: 리뷰어 친화적으로 쪼개기&lt;/h3&gt;
&lt;p&gt;모듈 분리를 하다 보면, 단순히 파일을 옮기고 &lt;code&gt;public&lt;/code&gt;을 붙였을 뿐인데도 &lt;strong&gt;수백 줄의 diff&lt;/strong&gt;가 생기곤 합니다. 이 상태에서 로직 변경까지 섞이면 리뷰어 입장에서 중요한 변경을 놓치기 쉬웠습니다.&lt;/p&gt;
&lt;p&gt;그래서 다음과 같이 커밋을 나누는 전략을 사용했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;의존성 분리 커밋&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;모듈로 분리하고자 하는 대상(컴포넌트, 클래스)과 엮인 의존성 코드를 먼저 분리&lt;/li&gt;
&lt;li&gt;커밋 메시지로 “의존성 정리”임을 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;파일 이동 커밋&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;대상 파일만 모듈로 &lt;strong&gt;이동&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Git이 rename/move를 인식하도록 변경 최소화&lt;/li&gt;
&lt;li&gt;리뷰어는 “이 커밋은 그냥 이동이구나” 하고 빠르게 넘어갈 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;public&lt;/code&gt; + &lt;code&gt;import&lt;/code&gt; 정리 커밋&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;이동된 파일에 필요한 &lt;code&gt;public&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;사용처에 &lt;code&gt;import ZComponent&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;커밋 메시지로 명시해서 “접근 제어자/임포트 정리 커밋”임을 알 수 있게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로직 변경 커밋&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;실제 로직 변경은 별도의 커밋으로 분리&lt;/li&gt;
&lt;li&gt;PR 설명에 “여기가 진짜 로직 변경 포인트”라고 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 나누니 리뷰어 입장에서 &lt;strong&gt;“어디를 집중해서 봐야 하는지”가 훨씬 명확&lt;/strong&gt;해졌고, 결과적으로 리뷰 속도와 품질이 함께 개선되었습니다.&lt;/p&gt;
&lt;h2&gt;3. Component 스토리북: 서버 드리븐 환경의 가시성 높이기&lt;/h2&gt;
&lt;p&gt;앞서 말한 두 번째 문제, 즉 &lt;strong&gt;서버 드리븐 환경에서의 가시성 부족&lt;/strong&gt;을 해결하기 위해 우리는 ZComponent 모듈에 **데모 타겟(스토리북 앱)**을 만들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-12-30-1/07.png"&gt;&lt;/p&gt;
&lt;h3&gt;3-1. 목표&lt;/h3&gt;
&lt;p&gt;Component 스토리북의 목표는 명확했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ZComponent 모듈에 어떤 컴포넌트들이 있는지 한눈에 파악&lt;/strong&gt;할 수 있을 것&lt;/li&gt;
&lt;li&gt;각 컴포넌트의 &lt;strong&gt;variation(상태, 옵션 조합)을 손쉽게 살펴볼 수 있을 것&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3-2. 구현 방식&lt;/h3&gt;
&lt;h4&gt;1) 데모 타겟 추가&lt;/h4&gt;
&lt;p&gt;ZComponent 모듈에 데모용 앱 타겟을 추가했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데모앱은 ZComponent 모듈을 import하여 &lt;strong&gt;컴포넌트만으로 화면을 구성&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;서버와는 완전히 독립된 형태로, 순수하게 “컴포넌트의 모습”만 보여주는 앱입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2) JSON 기반 Mock 데이터&lt;/h4&gt;
&lt;p&gt;각 컴포넌트가 바인딩하는 모델 데이터를 &lt;strong&gt;JSON 파일로 케이스별 저장&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;예: &lt;code&gt;GoodsGroupCell_default.json&lt;/code&gt;, &lt;code&gt;GoodsGroupCell_discounted.json&lt;/code&gt; …&lt;/li&gt;
&lt;li&gt;데모 앱에서는 이 JSON을 로딩해 모델로 디코딩한 뒤, 해당 컴포넌트에 바인딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이를 통해 다음이 가능해졌습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상태/옵션 조합이 많은 컴포넌트도 &lt;strong&gt;모든 케이스를 빠르게 순회&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;서버 응답 스펙과 무관하게, 순수하게 UI만 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3) 팀 전체의 컴포넌트 카탈로그로&lt;/h4&gt;
&lt;p&gt;이 데모앱은 iOS 개발자뿐만 아니라, 팀 전체의 “컴포넌트 카탈로그” 역할을 하게 되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;iOS 개발자&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;새 기능을 만들 때, 먼저 데모앱을 열어 &lt;strong&gt;이미 있는 컴포넌트로 구현 가능한지&lt;/strong&gt; 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서버 개발자&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;서버에서 어떤 타입을 내려줘야 어떤 컴포넌트가 그려지는지, 데모앱으로 스펙을 구체화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디자이너&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;현재 앱이 실제로 &lt;strong&gt;어떤 컴포넌트를 지원하는지&lt;/strong&gt;, 어떤 variation이 있는지 화면으로 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이전에는 “누군가의 머릿속”에만 있던 정보가, 이제는 &lt;strong&gt;누구나 실행해볼 수 있는 앱&lt;/strong&gt;으로 정리되었다는 점이 가장 큰 변화였습니다.&lt;/p&gt;
&lt;h2&gt;4. Component 스냅샷 테스트: QA 영향 범위를 시각화하기&lt;/h2&gt;
&lt;p&gt;세 번째 문제였던 &lt;strong&gt;QA 범위 파악의 어려움&lt;/strong&gt;을 해결하기 위해, 우리는 Component 모듈에 &lt;strong&gt;스냅샷 테스트&lt;/strong&gt;를 도입했습니다.&lt;/p&gt;
&lt;h3&gt;4-1. 스냅샷 테스트의 목표&lt;/h3&gt;
&lt;p&gt;Component 스냅샷 테스트의 목적은 명확합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Component 모듈 변경 시 &lt;strong&gt;다른 컴포넌트에 미치는 영향도&lt;/strong&gt;를 파악&lt;/li&gt;
&lt;li&gt;그 영향도를 &lt;strong&gt;한눈에 보기 쉽게 정리&lt;/strong&gt;하는 것&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4-2. 구현: uber/ios-snapshot-test-case + CI/CD&lt;/h3&gt;
&lt;p&gt;우리는 &lt;a href="https://github.com/uber/ios-snapshot-test-case"&gt;uber/ios-snapshot-test-case&lt;/a&gt; 라이브러리를 기반으로 스냅샷 테스트를 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-12-30-1/08.png"&gt;&lt;/p&gt;
&lt;p&gt;작동 방식은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-12-30-1/09.png"&gt;&lt;/p&gt;
&lt;p&gt;덕분에 PR에 다음과 같은 대화가 가능해졌습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-12-30-1/10.png"&gt;&lt;/p&gt;
&lt;p&gt;PR 작성자와 코드 리뷰어는 &lt;strong&gt;실제 UI 기준으로 어떤 컴포넌트들이 영향을 받았는지&lt;/strong&gt;를 PR 화면에서 바로 확인할 수 있게 되었고 QA에게 바로 전달할 수 있게 되었습니다.&lt;/p&gt;
&lt;h2&gt;정리하며: 우리가 해결하고 싶었던 세 가지 문제&lt;/h2&gt;
&lt;p&gt;Component 모듈, 데모앱, 스냅샷 테스트까지의 흐름을 통해, 처음에 이야기했던 세 가지 문제를 다음과 같이 정리할 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Feature 모듈 분리 전, 필수 요건&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;여러 지면에서 공통으로 쓰이는 UI 컴포넌트들을 ZComponent 모듈로 분리함으로써, 향후 피처 모듈 분리를 위한 기반을 마련했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서버 드리븐 환경에서의 가시성 부족&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;ZComponent 데모앱(스토리북)을 통해, 어떤 컴포넌트가 존재하고 어떤 variation이 있는지 &lt;strong&gt;한눈에 확인 가능한 카탈로그&lt;/strong&gt;를 만들었습니다.&lt;/li&gt;
&lt;li&gt;이제는 “비슷한 컴포넌트 또 만들 뻔했다”는 상황을 많이 줄일 수 있게 되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;영향범위·QA 범위 파악의 어려움&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Component 스냅샷 테스트를 도입하고, CI/CD와 연계해 PR 코멘트로 결과를 남기면서, &lt;strong&gt;UI 기준의 영향 범위&lt;/strong&gt;를 시각적으로 공유할 수 있게 되었습니다.&lt;/li&gt;
&lt;li&gt;“이번 변경이 어디까지 영향을 미쳤는지”를 말이 아닌 &lt;strong&gt;이미지로&lt;/strong&gt; 설명할 수 있게 된 것이 큰 차이였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;모듈화 여정은 여전히 진행 중이고, 완벽한 정답을 찾았다고 보기는 어렵습니다. 다만 이번 Component 모듈 작업을 통해,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;피처 모듈 분리를 위한 발판을 만들고,&lt;/li&gt;
&lt;li&gt;서버 드리븐 환경에서의 컴포넌트 가시성을 높이고,&lt;/li&gt;
&lt;li&gt;영향 범위를 이전보다 빠르게 파악할 수 있게 되었다는 점은 분명한 성과였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 글이 비슷한 고민을 하고 있는 팀들에게 “이런 접근도 가능하구나” 정도의 참고가 되면 좋겠습니다.&lt;/p&gt;
&lt;/div&gt;
</ns0:encoded></item><item><title>지그재그에 “포치의 선물가게”를 오픈하며</title><link>https://devblog.kakaostyle.com/ko/2025-11-23-1-pochi-game/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="col-12 col-lg-9" morss_own_score="2.975862068965517" morss_score="233.23828631138977"&gt;
&lt;p&gt;안녕하세요, 카카오스타일 지그재그 서비스 FE팀의 제이슨입니다.&lt;/p&gt;
&lt;p&gt;올해 저희는 지그재그 앱 내에 포치의 선물가게라는 게임 서비스를 오픈했습니다.
이 글에서는 포치의 선물가게 게임을 웹 환경에서 구현하면서 겪었던 기술적 선택들과 그 과정에서 했던 고민을 공유해보려고 합니다.&lt;/p&gt;
&lt;h2&gt;포치의 선물가게 게임이란?&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-11-23-1/1.png"&gt;&lt;/p&gt;
&lt;p&gt;포치의 선물가게 게임은 동일한 과일 두 개가 부딪히면 더 큰 과일로 합쳐지는 방식의 &lt;strong&gt;퍼즐형 물리 게임&lt;/strong&gt;입니다.
하늘에서 과일이 하나씩 떨어지고, 플레이어는 이를 좌우로 이동시키며 상자 안에 쌓아가는 구조로 게임이 진행됩니다.&lt;/p&gt;
&lt;p&gt;처음에는 씨앗처럼 작은 요소부터 시작해 점점 더 큰 요소로 성장하게 되고, 가장 마지막 단계인 별사탕까지 만들게 되면 클리어하게 됩니다.&lt;/p&gt;
&lt;p&gt;단순한 규칙처럼 보이지만, 요소의 크기나 무게, 충돌 타이밍에 따라 예상치 못한 상황이 생기기도 해 은근히 몰입하게 되는 매력이 있습니다.&lt;/p&gt;
&lt;h2&gt;기술 스택과 구현 방향&lt;/h2&gt;
&lt;p&gt;지그재그 웹 프론트엔드 환경은 &lt;strong&gt;React + TypeScript + Next.js&lt;/strong&gt; 기반으로 구성되어 있으며, 이번에 만든 게임 역시 지그재그 앱 내에서 동작하는 웹뷰 기반 서비스였기 때문에, 별도의 독립 앱으로 분리하지 않고 기존 서비스 안에서 구현하는 방향으로 진행했습니다.&lt;/p&gt;
&lt;p&gt;물리엔진을 사용한 게임을 직접 구현하게 된 건 처음이었기 때문에, 먼저 오픈소스로 공개된 여러 수박게임 클론들을 분석하며 구조를 파악했습니다.
그중 대부분이 2D 물리엔진인 &lt;a href="https://brm.io/matter-js/"&gt;Matter.js&lt;/a&gt;를 활용하고 있다는 점을 확인했고, 이를 기반으로 MVP를 빠르게 구성해보기로 결정했습니다.&lt;/p&gt;
&lt;h3&gt;Canvas 위에 물리 월드 구성&lt;/h3&gt;
&lt;p&gt;과일, 벽, 바닥은 각각 Matter.js의 &lt;code&gt;Bodies.circle&lt;/code&gt;, &lt;code&gt;Bodies.rectangle&lt;/code&gt; 등을 사용해 생성하고, &lt;code&gt;World.add&lt;/code&gt;를 통해 물리 월드에 등록했습니다.
Matter.js가 자체적으로 중력, 반발력, 충돌 등을 시뮬레이션해주는 덕분에, 간단한 설정만으로도 과일이 자연스럽게 떨어지고 굴러가는 느낌을 구현할 수 있었습니다.&lt;/p&gt;
&lt;h3&gt;게임 조건 처리 로직 (낙하, 병합, 충돌 판정)&lt;/h3&gt;
&lt;p&gt;사용자가 화면을 터치하거나 클릭하면, 그 위치를 기준으로 다음에 떨어질 과일의 위치를 실시간으로 이동시킬 수 있도록 구현했습니다.&lt;/p&gt;
&lt;p&gt;입력이 끝나는 시점(손을 떼는 시점)에는 해당 위치로 과일을 떨어뜨리고, Matter.js의 물리 시뮬레이션을 통해 자연스럽게 낙하와 충돌이 발생합니다.&lt;/p&gt;
&lt;p&gt;이후 &lt;code&gt;collisionStart&lt;/code&gt; 충돌 이벤트를 통해 과일끼리의 충돌을 감지하고, 두 과일이 같은 종류일 경우 기존 두 과일을 제거하고 같은 위치에 다음 단계의 과일이 새롭게 생성되도록 처리했습니다.&lt;/p&gt;
&lt;p&gt;이처럼 과일 병합, 게임 클리어 조건, 게임오버 판정 등 게임 진행에 필요한 주요 로직은 Matter.js 내부에서 처리하도록 구성했습니다.&lt;/p&gt;
&lt;h3&gt;React를 통한 게임 상태 표현&lt;/h3&gt;
&lt;p&gt;게임 로직은 모두 Matter.js에서 처리하고, 그 결과를 사용자에게 보여주는 역할은 React가 담당합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Matter.js에서 다음에 등장할 과일을 결정하면, React는 해당 정보를 받아 상단 UI에 미리보기 형태로 보여줍니다.&lt;/li&gt;
&lt;li&gt;과일이 병합되거나 점수가 올라갈 경우, React는 게임 상태를 기반으로 현재 점수와 게임 상황을 실시간으로 업데이트합니다.&lt;/li&gt;
&lt;li&gt;수박이 만들어지거나 상자 위까지 과일이 쌓이는 등 게임오버 조건이 충족되면, Matter.js가 해당 상태를 판별합니다. 그러면 React가 클리어 또는 게임오버 화면을 보여줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;사용자가 재시작 버튼을 클릭하면, React가 해당 이벤트를 Matter.js로 전달하여 물리 월드를 초기화하고 게임을 새로 시작합니다.&lt;/p&gt;
&lt;p&gt;이처럼 React는 게임의 판단이나 로직에는 직접 관여하지 않고, &lt;strong&gt;Matter.js가 판단한 결과를 사용자에게 어떻게 보여줄지(UI)와, 사용자의 입력을 다시 Matter.js로 전달하는 연결 지점 역할만 담당하도록 구조를 분리했습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;덕분에 물리 연산과 렌더링 사이의 책임이 명확해졌고, 게임 로직과 UI가 서로 영향을 주지 않도록 관리하기 수월했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-11-23-1/2.png"&gt;&lt;/p&gt;
&lt;h2&gt;Phaser.js로의 전환&lt;/h2&gt;
&lt;p&gt;Matter.js는 기본적인 게임 플레이를 구현하는 데에는 충분했습니다.
중력, 충돌과 같은 물리 연산은 쉽게 구현할 수 있었고, MVP 단계에서도 비교적 큰 문제 없이 개발이 진행됐습니다.&lt;/p&gt;
&lt;p&gt;하지만 게임을 만들다 보니, 기능은 동작하는데 &lt;strong&gt;어딘가 “게임 같다"는 느낌이 부족하다&lt;/strong&gt;는 생각이 들기 시작했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;과일이 병합될 때 터지는 효과&lt;/li&gt;
&lt;li&gt;게임오버 시 쌓인 과일이 하나씩 사라지는 연출&lt;/li&gt;
&lt;li&gt;마지막 단계 과일(별사탕)이 반짝거리는 효과&lt;/li&gt;
&lt;li&gt;사운드 및 진동을 활용한 피드백&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 요소들이 들어가야 게임의 완성도를 높일 수 있다고 판단했지만, &lt;strong&gt;Matter.js의 기본 렌더러만으로는 이 연출들을 구현하기 어려웠습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그래서 당시 몇 가지 대안을 고민하게 되었는데요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Canvas 위에 &lt;strong&gt;별도 이펙트용 Canvas 레이어&lt;/strong&gt;를 추가&lt;/li&gt;
&lt;li&gt;DOM/SVG 애니메이션으로 일부 연출을 대체&lt;/li&gt;
&lt;li&gt;렌더링 루프를 직접 만들어 &lt;strong&gt;Matter.js를 물리 엔진으로만 사용&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각각 시도해 볼 수 있는 방식이었지만, 성능이나 구조 복잡도, 이후 유지보수를 생각하면 작은 미니게임에는 과한 선택처럼 느껴졌습니다.&lt;/p&gt;
&lt;p&gt;이 과정에서 정리된 요구사항은 다음과 같았습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“렌더링 루프를 직접 만들지 않으면서, 게임에 필요한 기능은 프레임워크에서 제공해주고, 게임 구현에 필요한 물리 연산까지 제공되면 좋겠다.”&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;그때 떠올랐던 엔진이 &lt;strong&gt;Phaser.js&lt;/strong&gt;였습니다.
예전에 개인적으로 게임 엔진의 라이프 사이클을 공부하면서 Phaser를 알게 되었는데, Phaser가 장면 관리 및 이벤트 연출 시스템을 제공하면서도 &lt;strong&gt;물리 엔진으로 Matter.js를 그대로 사용할 수 있다는 점&lt;/strong&gt;이 요구사항을 모두 만족하는 선택지였습니다.&lt;/p&gt;
&lt;p&gt;물론 전환 과정이 매끄럽지만은 않았습니다.
개발 기간은 한 달 정도로 잡혀 있었고, 이미 일정의 많은 부분을 소진한 상태에서 구조를 바꾸는 일은 꽤 큰 리스크였기 때문입니다.&lt;/p&gt;
&lt;p&gt;또 기존 React + Matter.js 구조에 Phaser.js를 더하는 과정에서 장면 구조나 라이프사이클, 상태 동기화를 다시 설계해야 했습니다.&lt;/p&gt;
&lt;p&gt;그래도 여러 번 검토하며 적용 범위를 조정해 나갔고, “이 선택이 게임의 완성도를 확실하게 올릴 수 있는가"를 기준으로 조심스럽게 전환을 진행하였고, 결과적으로 Phaser.js 도입은 큰 도움이 되었습니다.&lt;/p&gt;
&lt;p&gt;과일이 병합될 때 터지는 효과, 스프라이트 시트 애니메이션을 통해 별사탕이 귀엽게 반짝거리는 효과 등 Matter.js만으로는 어려웠던 게임다운 느낌을 구현할 수 있었습니다.&lt;/p&gt;
&lt;h2&gt;게임을 만들면서 고민했던 부분들&lt;/h2&gt;
&lt;p&gt;아래는 구현 과정에서 기억에 남았던 고민을 몇 가지 정리해 보았습니다.&lt;/p&gt;
&lt;h3&gt;1. 왜 과일 이미지는 모두 “원형"일까?&lt;/h3&gt;
&lt;p&gt;게임을 구현하면서 가장 먼저 들었던 의문 중 하나가 “왜 대부분의 수박게임은 과일 이미지가 전부 원형으로 되어있을까?“였습니다.&lt;/p&gt;
&lt;p&gt;처음에는 단순히 캐주얼하게 보여주려는 디자인적인 선택이라고 생각했는데, 조금 더 찾아보니 물리 엔진의 충돌 판정 방식 때문이라는 걸 알게 되었습니다.&lt;/p&gt;
&lt;p&gt;Matter.js에서 픽셀 단위로 정밀 충돌을 계산하려면 이미지의 형태 전체를 매 프레임 분석해야 하는데, 이는 연산 비용이 크게 증가하게 됩니다.&lt;/p&gt;
&lt;p&gt;반면 원형 충돌은 반지름 기반의 단순 계산으로 처리되기 때문에 안정적이고 빠르게 동작합니다.
그래서 대부분의 수박게임이 자연스럽게 원형 에셋을 선택하고 있었던 것 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;정밀도&lt;/th&gt;
&lt;th&gt;구현 난이도&lt;/th&gt;
&lt;th&gt;성능&lt;/th&gt;
&lt;th&gt;추천 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Canvas로 알파 추출 후 폴리곤 변환&lt;/td&gt;
&lt;td&gt;매우 높음&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;느릴 수 있음&lt;/td&gt;
&lt;td&gt;고정밀이 필요한 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;반지름 보정한 원형 바디 사용&lt;/td&gt;
&lt;td&gt;충분히 괜찮음&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;td&gt;일반 게임, 수박게임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG 기반 변환&lt;/td&gt;
&lt;td&gt;정확&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;SVG 필요&lt;/td&gt;
&lt;td&gt;스프라이트가 벡터일 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이런 배경 때문에 저희도 성능과 안정성을 우선해 모든 과일 이미지를 원형으로 제작하는 방향을 선택했습니다.&lt;/p&gt;
&lt;h3&gt;2. 재현 가능한 시드 기반 랜덤 생성 (Mulberry32 PRNG)&lt;/h3&gt;
&lt;p&gt;초기 구현에는 단순히 &lt;code&gt;Math.random()&lt;/code&gt;을 사용해 다음 과일을 생성하고 있었지만, 이렇게 하면 게임 난이도 조절이 어렵고 플레이 패턴이 매번 달라져 밸런스 테스트가 힘들었습니다. 무엇보다 정상적인 플레이인지 검증하기도 어려운 구조였습니다.&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위해 “랜덤이지만 재현 가능한 방식은 없을까?“를 고민했고, 그 과정에서 &lt;strong&gt;시드 기반으로 동작하는 PRNG(Pseudo-Random Number Generator) 개념을 처음 접하게 되었습니다.&lt;/strong&gt;
관련 자료를 찾아보던 중 구조가 단순하고 가벼운 &lt;a href="https://github.com/cprosche/mulberry32"&gt;Mulberry32&lt;/a&gt;가 눈에 띄었고, 현재 게임 구조와 잘 맞다고 판단해 이를 적용하게 되었습니다.&lt;/p&gt;
&lt;p&gt;Mulberry32로 전환하면서 동일한 시드 값 사용시 동일한 순서로 값을 생성하는 것을 보장할 수 있었고, 디버깅이나 테스트 과정에서도 훨씬 안정적인 구조를 만들 수 있었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// Source: https://github.com/cprosche/mulberry32
&lt;/span&gt;&lt;span&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;mulberry32&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;: &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
&lt;/span&gt;  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;()&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;let&lt;/span&gt; &lt;span&gt;t&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;a&lt;/span&gt; &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;0x6d2b79f5&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;t&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;Math&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;imul&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;t&lt;/span&gt; &lt;span&gt;^&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;t&lt;/span&gt; &lt;span&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span&gt;15&lt;/span&gt;&lt;span&gt;),&lt;/span&gt; &lt;span&gt;t&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;t&lt;/span&gt; &lt;span&gt;^=&lt;/span&gt; &lt;span&gt;t&lt;/span&gt; &lt;span&gt;+&lt;/span&gt; &lt;span&gt;Math&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;imul&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;t&lt;/span&gt; &lt;span&gt;^&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;t&lt;/span&gt; &lt;span&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span&gt;7&lt;/span&gt;&lt;span&gt;),&lt;/span&gt; &lt;span&gt;t&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;61&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;((&lt;/span&gt;&lt;span&gt;t&lt;/span&gt; &lt;span&gt;^&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;t&lt;/span&gt; &lt;span&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span&gt;14&lt;/span&gt;&lt;span&gt;))&lt;/span&gt; &lt;span&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;/&lt;/span&gt; &lt;span&gt;4294967296&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;};&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; &lt;span&gt;rand&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;mulberry32&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;);&lt;/span&gt; &lt;span&gt;// seed: 1
&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;rand&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;
&lt;span&gt;// 동일한 생성 순서를 보장
&lt;/span&gt;&lt;span&gt;// output: 0.6270739405881613 -&amp;gt; 0.002735721180215478 -&amp;gt; 0.5274470399599522 ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;3. Next.js + Phaser.js 환경에서 만난 SSR 문제&lt;/h3&gt;
&lt;p&gt;사소한 부분이긴 하지만, Matter.js에서 Phaser.js로 전환하는 과정에서 SSR 에러가 발생했습니다.&lt;/p&gt;
&lt;p&gt;확인해 보니 Phaser.js 내부 모듈이 초기화되는 과정에서 브라우저 환경에서만 존재하는 &lt;code&gt;window.navigator.userAgent&lt;/code&gt; 객체를 참조하고 있었고, Next.js의 SSR 단계에서는 해당 객체가 없기 때문에 에러가 발생한 것이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-11-23-1/3.png"&gt;&lt;/p&gt;
&lt;p&gt;이 문제는 Phaser.js를 사용하고 있는 컴포넌트를 Next.js의 dynamic 함수를 사용하여 브라우저 환경에서만 컴포넌트를 로드하도록 분리해 해결했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// game 컴포넌트 내부에서 import Phaser from 'phaser' 를 사용.
&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;Game&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;dynamic&lt;/span&gt;&lt;span&gt;(()&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;import&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'@/components/game'&lt;/span&gt;&lt;span&gt;),&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;span&gt;ssr&lt;/span&gt;: &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;4. 캔버스 DPR(Device Pixel Ratio) 처리 문제&lt;/h3&gt;
&lt;p&gt;Matter.js를 사용할 때까지만 해도 큰 문제가 없었는데, Phaser.js로 전환한 뒤 과일 이미지가 고해상도 기기에서 흐릿하게 보이는 현상이 발견되었습니다.
기기마다 DPR(Device Pixel Ratio)이 다르다는 것은 알고 있었지만, 왜 두 라이브러리에서 결과가 다르게 나오는지 원인을 파악하기 시작했습니다.&lt;/p&gt;
&lt;p&gt;확인해보니 두 엔진의 &lt;strong&gt;DPR 처리 방식 차이&lt;/strong&gt;가 문제의 원인이었습니다.&lt;/p&gt;
&lt;p&gt;Matter.js는 DPR 값을 기준으로 캔버스의 실제 렌더링 크기를 자동으로 보정해 주는 반면, Phaser.js는 별도 설정이 없으면 논리적 캔버스 크기(400×600)를 그대로 사용해 렌더링했습니다. 그 결과, DPR이 높은 디바이스에서는 픽셀이 뭉개져 보이는 현상이 발생했습니다.&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위해 Phaser.js 초기 설정에서 window.devicePixelRatio 값을 직접 사용해 캔버스를 설정했습니다.
예를 들어 DPR이 3인 경우에는 &lt;strong&gt;1200×1800&lt;/strong&gt;처럼 물리 캔버스를 더 크게 생성하고, 내부 오브젝트의 크기와 위치도 해당 배율에 맞춰 조정해 해결했습니다.&lt;/p&gt;
&lt;h3&gt;5. 웹의 느낌 덜어내기&lt;/h3&gt;
&lt;p&gt;프로젝트 달성을 위해 꼭 필요한 기능들은 아니었지만, 개인적으로 이번 게임을 만들면서 웹의 느낌은 최대한 덜어내고 사용자가 실제로 ‘게임을 하고 있다’라는 느낌을 받을 수 있도록 신경을 썼던 것 같습니다.&lt;/p&gt;
&lt;p&gt;아래는 그 과정에서 챙겨봤던 작은 디테일들입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. iOS 뒤로가기 제스처 방지:&lt;/strong&gt;
iOS는 화면 왼쪽 모서리에서 스와이프하면 뒤로가기가 실행되는데, 과일을 드래그하는 과정에서 이 제스처가 의도치 않게 발동될 가능성이 있다고 판단했습니다.&lt;/p&gt;
&lt;p&gt;게임 플레이 중 갑자기 페이지가 닫히는 것은 사용자 경험 상 큰 문제기 때문에, 이를 방지하기 위해 모서리 영역에서 발생하는 터치 제스처를 캡처해 뒤로가기가 실행되지 않도록 처리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. iOS 상하 스크롤 바운스 제거:&lt;/strong&gt;
iOS 웹뷰에서는 화면 가장자리에서 스크롤 할 때 자연스럽게 바운스가 발생하는데, 이런 동작은 게임의 몰입감을 떨어뜨릴 수 있어 &lt;code&gt;overscroll-behavior-y: none&lt;/code&gt;을 적용해 상하 스크롤 바운스가 발생하지 않도록 수정했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;body&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;overscroll-behavior-y&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;none&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;3. 진동 피드백 설계:&lt;/strong&gt;
BGM과 효과음을 사용할 수 없는 상황에서 피드백의 대부분은 진동에 의존해야 했습니다. 처음에는 모든 진동의 시간을 100ms로 통일했지만 직접 플레이해 보니 꽤 이질감이 느껴졌습니다.&lt;/p&gt;
&lt;p&gt;이후 Android의 &lt;a href="https://m2.material.io/design/platform-guidance/android-haptics.html"&gt;Material Design 햅틱 가이드&lt;/a&gt;를 참고해 행동별로 진동 시간을 구분했고, 그 결과 실제 플레이 피드백도 훨씬 자연스러워졌습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;버튼 클릭: 10ms&lt;/li&gt;
&lt;li&gt;스위치 토글: 30ms&lt;/li&gt;
&lt;li&gt;과일 병합 및 터질 때: 20ms&lt;/li&gt;
&lt;li&gt;게임오버: 500ms&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;플레이 도중 갑자기 게임오버 처리되는 문제&lt;/h2&gt;
&lt;p&gt;서비스 오픈 이후, “게임을 플레이 중인데 갑자기 게임오버가 된다"라는 VOC가 계속 들어왔습니다.
처음에는 일부 상황에서만 발생하는 문제라고 생각했지만, 확인해 보니 게임오버 판정 &lt;strong&gt;로직 자체에 구조적인 문제가 있었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;원인을 따라가 보니 게임오버 판정은 크게 두 가지 흐름에 의존하고 있었고, 이 둘이 맞물리면서 예기치 않은 게임오버가 발생하고 있었습니다.&lt;/p&gt;
&lt;h3&gt;문제 원인 1: 과일 생성 직후 타이머 기반 판정&lt;/h3&gt;
&lt;p&gt;게임 구조상 미리보기 과일은 항상 &lt;strong&gt;게임오버 기준선 위에서 생성&lt;/strong&gt;된 뒤 아래로 떨어집니다. 그래서 생성 직후 바로 게임오버 처리가 되지 않도록 &lt;strong&gt;딜레이 타이머&lt;/strong&gt;를 두고 있었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;fruit&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setTriggableAfterDelay&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1000&lt;/span&gt;&lt;span&gt;);&lt;/span&gt; &lt;span&gt;// 일정 시간이 지나면 게임오버 판정 대상이 됨
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;또 과일을 떨어뜨릴 때마다 전체적인 게임오버 판정도 일정 시간 뒤에 수행되는 구조였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;resetGameOverCheckTimer() {&lt;/span&gt;
  &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverTimer&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;delayedCall&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1200&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;()&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;checkGameOver&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;
  &lt;span&gt;});&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;하지만 &lt;strong&gt;타이머는 기기의 성능, 프레임 드랍, 백그라운드 복귀 등 외부 환경을 그대로 받기 때문에&lt;/strong&gt;, 다음과 같은 문제가 발생했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;과일이 아직 떨어지는 중인데 타이머 타이밍이 맞아버리면 그대로 게임오버&lt;/li&gt;
&lt;li&gt;타이머가 과일의 물리 상태를 고려하지 않음&lt;/li&gt;
&lt;li&gt;실제 플레이 상황과 정확히 맞지 않는 시점에 판정이 실행됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;타이머 시간을 3,000ms로 늘려보기도 했지만, 이는 근본적인 문제 해결이라기보단 &lt;strong&gt;단순히 지연만 늘리는 임시 조치&lt;/strong&gt;에 불과했습니다.&lt;/p&gt;
&lt;h3&gt;문제 원인 2: 물리 상태를 고려하지 않은 y 좌표 기반 판정&lt;/h3&gt;
&lt;p&gt;게임오버 판정은 모든 과일의 y 좌표를 기준선과 비교하는 단순한 방식이었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;isOverTopLine&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matter&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;world&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getAllBodies&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;some&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isStatic&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameObject&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;body&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;position&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;y&lt;/span&gt; &lt;span&gt;&amp;lt;=&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverLineY&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;문제는 이 방식이 아래와 같은 &lt;strong&gt;정상적인 물리 현상까지 게임오버로 처리&lt;/strong&gt;하고 있었다는 점입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;과일이 떨어지는 중에 기준선을 &lt;strong&gt;순간적으로 스치는 경우&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;다른 과일 위에 안착하기 전 &lt;strong&gt;덜컹거리며 흔들리는 순간&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;물리 엔진 특성상 발생하는 &lt;strong&gt;미세한 좌표 튐&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 물리적으로 전혀 문제없는 상황에서도 “y 좌표가 기준선을 넘었다"라는 하나의 조건만으로 게임오버가 발생하고 있었습니다.&lt;/p&gt;
&lt;h3&gt;문제 해결: 과일 상태 기반 구조로 변경&lt;/h3&gt;
&lt;p&gt;이 문제를 해결하기 위해 기존의 “&lt;strong&gt;타이머 + y 좌표”&lt;/strong&gt; 기반 구조에서 벗어나, 과일의 **현재 상태(state)**를 기준으로 판정하는 방식으로 변경했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 과일 상태 정의:&lt;/strong&gt;
과일은 &lt;code&gt;dropping&lt;/code&gt; / &lt;code&gt;settled&lt;/code&gt; 두 가지 상태를 갖게 되며, 새로 생성된 과일은 항상 &lt;code&gt;dropping&lt;/code&gt; 상태로 시작합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;FruitState&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;'dropping'&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;'settled'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;class&lt;/span&gt; &lt;span&gt;FruitObject&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;Phaser&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Physics&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Matter&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Image&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;span&gt;fruitState&lt;/span&gt;: &lt;span&gt;FruitState&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;'dropping'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/span&gt;
&lt;span&gt;  &lt;span&gt;setFruitState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;state&lt;/span&gt;: &lt;span&gt;FruitState&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
&lt;/span&gt;    &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fruitState&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;state&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;isSettled() {&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fruitState&lt;/span&gt; &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'settled'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;2. 충돌 이벤트에서 안착 상태로 전환:&lt;/strong&gt;
충돌 이벤트를 통해 바닥이나 다른 과일에 안착했을 때 &lt;code&gt;settled&lt;/code&gt;로 전환합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;handleCollisionStart&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;event&lt;/span&gt;: &lt;span&gt;Phaser.Physics.Matter.Events.CollisionStartEvent&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
&lt;/span&gt;  &lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;pair&lt;/span&gt; &lt;span&gt;of&lt;/span&gt; &lt;span&gt;event&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pairs&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;bodyA&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;bodyB&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pair&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;fruitA&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;bodyA&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameObject&lt;/span&gt; &lt;span&gt;instanceof&lt;/span&gt; &lt;span&gt;FruitObject&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;&lt;span&gt;bodyA&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isStatic&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;bodyA.gameObject&lt;/span&gt; : &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;fruitB&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;bodyB&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameObject&lt;/span&gt; &lt;span&gt;instanceof&lt;/span&gt; &lt;span&gt;FruitObject&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;&lt;span&gt;bodyB&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isStatic&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;bodyB.gameObject&lt;/span&gt; : &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/span&gt;
    &lt;span&gt;// 과일이 바닥 또는 다른 과일과 부딪힌 경우 → 안착 상태로 전환
&lt;/span&gt;    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;fruitA&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;&lt;span&gt;fruitA&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isSettled&lt;/span&gt;&lt;span&gt;())&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;bodyB&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;label&lt;/span&gt; &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'Ground'&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; &lt;span&gt;fruitB&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;fruitA&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setFruitState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'settled'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;fruitB&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;&lt;span&gt;fruitB&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isSettled&lt;/span&gt;&lt;span&gt;())&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;bodyA&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;label&lt;/span&gt; &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'Ground'&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; &lt;span&gt;fruitA&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;fruitB&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setFruitState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'settled'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;// (아래는 과일 병합 로직 등 다른 처리들…)
&lt;/span&gt;  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;3. settled 과일만 게임오버 판정에 포함:&lt;/strong&gt;
기존에는 &lt;strong&gt;모든 과일의 y 좌표를 기준선과 비교&lt;/strong&gt;했지만, 상태 기반으로 변경한 뒤에는 &lt;code&gt;settled&lt;/code&gt; 상태인 과일만 판정에 포함하도록 수정했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// 현재 게임오버 라인 위에 settled 상태의 과일이 있는지 체크
&lt;/span&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;hasSettledFruitOverLine&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;Array&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;activeFruits&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;some&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;fruit&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;fruit&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isSettled&lt;/span&gt;&lt;span&gt;())&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;const&lt;/span&gt; &lt;span&gt;fruitTopY&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;fruit&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;y&lt;/span&gt; &lt;span&gt;-&lt;/span&gt; &lt;span&gt;fruit&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;radius&lt;/span&gt; &lt;span&gt;/&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt; &lt;span&gt;fruitTopY&lt;/span&gt; &lt;span&gt;&amp;lt;=&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverLineY&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;});&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그리고 이 함수는 Phaser Scene의 &lt;code&gt;update()&lt;/code&gt; 루프와 게임오버 유예 타이머에서 같이 사용됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;update() {&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isGameActive&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;isOverLine&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hasSettledFruitOverLine&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isOverLine&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverTimer&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// 처음 기준선을 넘은 순간 → 유예 타이머 시작
&lt;/span&gt;    &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;startGameOverTimer&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;isOverLine&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverTimer&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// 다시 기준선 아래로 내려가면 → 타이머/연출 모두 취소
&lt;/span&gt;    &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverTimer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;remove&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;
    &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverTimer&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;startGameOverTimer&lt;/code&gt; 내부에서도 마지막으로 한 번 더 상태를 확인한 뒤 정말로 게임오버로 처리할지 결정합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;startGameOverTimer() {&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverTimer&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverTimer&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;delayedCall&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GAME_OVER_DELAY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;()&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// 타이머가 끝난 시점에도 여전히 라인 위에 settled 과일이 있을 때만 게임오버
&lt;/span&gt;    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hasSettledFruitOverLine&lt;/span&gt;&lt;span&gt;())&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setGameState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'GAME_OVER'&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gameOverTimer&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;});&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 구조를 적용한 뒤로는 “잠깐 스쳤다"라는 이유로 갑자기 게임오버가 나는 상황이 사라졌고, 플레이 흐름을 더 정확히 반영한 안정적인 게임오버 판정이 가능해졌습니다.&lt;/p&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;이번 포치의 선물가게 게임 개발은 한 달이라는 짧은 기간이었지만, 그 안에서 정말 다양한 경험을 해볼 수 있었던 프로젝트였습니다.
물리 엔진, 연출, 게임 로직처럼 평소 웹 개발에서는 쉽게 접하기 어려운 영역을 다루면서 새로운 관점도 얻을 수 있었고, 서비스 디테일을 더 깊게 들여다보는 계기도 되었습니다.&lt;/p&gt;
&lt;p&gt;물론 쉽지 않은 순간들도 있었지만, 문제를 하나씩 해결해 나가고 사용자들이 게임을 재미있게 즐겨주셨다는 피드백을 봤을 때 큰 보람도 느낄 수 있었습니다.&lt;/p&gt;
&lt;p&gt;앞으로도 더 나은 사용자 경험을 위해 꾸준히 고민하고 시도해 보겠습니다.
긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;p&gt;아래는 실제 게임 플레이 영상입니다.&lt;/p&gt;


&lt;/div&gt;
</ns0:encoded></item><item><title>코드 사례로 보는 Domain-Driven 헥사고날 아키텍처</title><link>https://devblog.kakaostyle.com/ko/2025-03-21-1-domain-driven-hexagonal-architecture-by-example/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="col-12 col-lg-9" morss_own_score="3.0" morss_score="102.0"&gt;
&lt;p&gt;안녕하세요. 지그재그 서비스팀 로빈입니다.🙂
오늘은 저희 팀에서 관리하고 있는 상품상세페이지(Product Detail Page. 이하 PDP) 서비스의 프로젝트 아키텍처에 대해 간단히 소개해보려고 합니다.&lt;/p&gt;
&lt;p&gt;PDP 서비스는 2024년부터 “Domain-Driven 헥사고날 아키텍처"의 프로젝트 구조를 띠고 있는데요, 이렇게 운영한지 어느덧 1년을 향해 가고 있습니다.
국내외 여러 테크 블로그들을 돌아다녀보면 헥사고날 아키텍처란 무엇인지, DDD와 애그리거트는 무엇인지, 이미 그 개념들에 대해 충분히 잘 설명된 자료들을 접할 수 있습니다.
그래서 이 글에서는 아키텍처와 디자인 패턴에 대한 개념 설명보다는 실제 운영 경험을 통해 장점으로 느꼈던 부분들을 코드 예시와 함께 소개해볼까합니다.&lt;/p&gt;
&lt;h2&gt;PDP 프로젝트의 아키텍처&lt;/h2&gt;
&lt;p&gt;우선 아키텍처 구조가 어떻게 되어있는지 그림으로 표현해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://devblog.kakaostyle.com/img/content/2025-03-21-1/1.png"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;헥사고날 아키텍처의 핵심이 되는 인터페이스 Port 와 구현체인 Adapter 가 존재합니다.&lt;/li&gt;
&lt;li&gt;Hexagon 의 중심인 도메인 서비스 로직쪽에서는 Use Case 로 정보를 주고 받는 행위들이 존재합니다. Use Case 는 인터페이스로 되어있으며 이를 실제로 구현하는 Service 클래스가 존재합니다.&lt;/li&gt;
&lt;li&gt;실제 도메인 객체를 핸들링할때는 상위에 애그리거트 루트를 두어 그룹화하여 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러 전시 지면 중에서도 특히 PDP 는 하나의 페이지에 여러 마이크로서비스에서 제공하는 수많은 정보들을 가져와 노출해야하므로 도메인 객체를 그대로 사용하지 않고 DDD 에서 중요한 개념인 애그리거트 모델을 접목하여 그룹핑하여 관리하는 것이 특징입니다.
PDP는 단순히 상품 정보를 표시하는 페이지가 아니라, 약 20여 개의 마이크로서비스와 실시간으로 데이터를 주고받으며 사용자에게 필요한 정보를 최적의 형태로 가공해 제공해야 합니다.
따라서 가장 우선적으로 도메인 로직에 대한 불필요한 변경을 최소화하려는 목표가 있으며 이때 중요하게 생각한 키워드가 인터페이스(Port &amp;amp; Use Case)와 애그리거트입니다.&lt;/p&gt;
&lt;p&gt;이제 코드 예시가 있는 본론으로 가보겠습니다.&lt;/p&gt;
&lt;h2&gt;Q1. “애그리거트 모델이 접목된 헥사고날 아키텍처의 코드는 어떻게 생겼나요?”&lt;/h2&gt;
&lt;p&gt;아래 코드는 PDP 를 렌더링하기 위해 UI Component 목록을 반환하는 서버 API 의 일부 예시입니다.
헥사고날 아키텍처에서 Port 와 Use Case 가 어떻게 동작하는지 코드 플로우를 통해 확인할 수 있습니다.
외부 요청을 받았을 때 Controller → Input Port → Domain → Output Port 흐름으로 진행되며, 응답 플로우 또한 다시 역방향으로 진행됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;#1. Controller (진입점)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;@RestController&lt;/span&gt;
&lt;span&gt;@RequestMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"/pdp"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpController&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpPageAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pdpPagePort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpPagePort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;@GetMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"/{productId}"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getPdpPage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;@PathVariable&lt;/span&gt; &lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;runBlocking&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;pdpPagePort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generatePage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;#2. Driving Adapter (Input Port 의 구현체)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpPageAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pageUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpPageUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;LineMarginUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPagePort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;generatePage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;coroutineScope&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;// Step 1. 애그리거트(Aggregate) 루트 객체 생성
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pdpAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPdpAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Step 2. Server-Driven UI 컴포넌트 생성 및 배치
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;page&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pageUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generatePage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAgg&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Step 3. 생성된 UI 컴포넌트들을 기준으로 컴포넌트 간 라인과 마진을 적용한다.
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;prettyPage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;applyLineAndMargin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;return&lt;/span&gt;&lt;span&gt;@coroutineScope&lt;/span&gt; &lt;span&gt;prettyPage&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;#3. Domain Use Case (애그리거트 루트를 획득하는 과정)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpAggService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;catalogAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;CatalogAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;shopAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ShopAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;priceAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PriceAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;reviewAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ReviewAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;contentAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ContentAggUseCase&lt;/span&gt;
&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpAggUseCase&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getPdpAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;coroutineScope&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;catalogAggDeferred&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;catalogAggUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCatalogAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;shopAggDeferred&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;shopAggUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getShopAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;priceAggDeferred&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;priceAggUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPriceAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;reviewAggDeferred&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;reviewAggUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getReviewAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;contentAggDeferred&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;contentAggUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;// 이쪽 코드를 대상으로 조금 더 자세히 들여다보겠습니다.
&lt;/span&gt;
        &lt;span&gt;PdpAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
            &lt;span&gt;id&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;catalogAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;catalogAggDeferred&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;
            &lt;span&gt;shopAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shopAggDeferred&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;
            &lt;span&gt;priceAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;priceAggDeferred&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;
            &lt;span&gt;reviewAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;reviewAggDeferred&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;
            &lt;span&gt;contentAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;contentAggDeferred&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;
        &lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;#4. Domain Use Case (하위 애그리거트를 획득하는 과정)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Notes: PDP 컨텐츠에는 여러 정보들이 있겠지만 &lt;code&gt;PDP 배너 정보&lt;/code&gt;를 가져오는 예시 하나만 나열하였습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;ContentAggService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerPrimaryAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;
&lt;span&gt;):&lt;/span&gt; &lt;span&gt;ContentAggUseCase&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpBanners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toDomainObject&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;#5. Driven Adapter (Output Port 의 구현체)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// 공통 Query Port
&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;QueryPort&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 공통 Command Port
&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;CommandPort&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 배너 조회 Output Port
&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;QueryPort&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;

&lt;span&gt;// 배너 저장 Output Port
&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;CommandPort&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;

&lt;span&gt;// Redis 를 통해 정보를 조회하거나 저장하는 output port 구현체
&lt;/span&gt;&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerRedisAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;redisTemplate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;RedisTemplate&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;redisTemplate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;opsForValue&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdp-banner:&lt;/span&gt;&lt;span&gt;$productId&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;redisTemplate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;opsForValue&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdp-banner:&lt;/span&gt;&lt;span&gt;$productId&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 외부 API 호출을 통해 정보를 조회하는 output port 구현체
&lt;/span&gt;&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerApiAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;webClient&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;WebClient&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;webClient&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;
            &lt;span&gt;.&lt;/span&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"https://external-api.com/content/&lt;/span&gt;&lt;span&gt;$productId&lt;/span&gt;&lt;span&gt;/banners"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
            &lt;span&gt;.&lt;/span&gt;&lt;span&gt;retrieve&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;
            &lt;span&gt;.&lt;/span&gt;&lt;span&gt;bodyToMono&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;object&lt;/span&gt; &lt;span&gt;: &lt;/span&gt;&lt;span&gt;ParameterizedTypeReference&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;()&lt;/span&gt; &lt;span&gt;{})&lt;/span&gt;
            &lt;span&gt;.&lt;/span&gt;&lt;span&gt;block&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Redis 캐시 -&amp;gt; 외부 API 호출 순서로 값을 취하는 output port 구현체
&lt;/span&gt;&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerPrimaryAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerRedisAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;redisQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerRedisAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;redisCommandPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerApiAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;redisQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// Redis 조회
&lt;/span&gt;            &lt;span&gt;?:&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;also&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;// Redis에 없을 때, API 호출
&lt;/span&gt;                &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isNotEmpty&lt;/span&gt;&lt;span&gt;())&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
                    &lt;span&gt;redisCommandPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;it&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// API 데이터를 Redis에 저장
&lt;/span&gt;                &lt;span&gt;}&lt;/span&gt;
            &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/*
&lt;/span&gt;&lt;span&gt; * Notes : PrimaryAdapter 의 경우 외부 호출에 대한 우선 순위 결정 로직이 들어가있으므로 이러한 경우, OutputPort 가 아닌 UseCase 를 통한 Service 클래스에서 처리해도 무방합니다.
&lt;/span&gt;&lt;span&gt; */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PDP 서버는 UI 서버드리븐 처리를 위해 다음과 같이 크게 세번의 동기적/순차적인 스텝을 거치는데요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Step 1. 애그리거트(Aggregate) 루트 객체 생성&lt;/li&gt;
&lt;li&gt;Step 2. Server-Driven UI 컴포넌트 생성 및 배치&lt;/li&gt;
&lt;li&gt;Step 3. 생성된 UI 컴포넌트들을 기준으로 컴포넌트 간 라인과 마진을 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위 예시로 든 코드의 경우 도메인 모델 즉, 애그리거트 루트를 획득하는 Step 1 과정 중심으로 코드 예시를 보았습니다.&lt;/p&gt;
&lt;p&gt;이제 위 코드를 기준으로 하여 헥사고날 아키텍처에서 어떠한 이점들이 있는지 하나씩 살펴보겠습니다. 🙂&lt;/p&gt;
&lt;h2&gt;Q2. “헥사고날 아키텍처를 쓰면 무슨 장점이 있어요?”&lt;/h2&gt;
&lt;p&gt;헥사고날 아키텍처로 운영하면 상대적으로 얻는 이점들이 있습니다&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;첫째, 비즈니스 로직이 외부 호출과 명확히 분리되어 있어서, 코드 유지보수가 절감되며 도메인 로직이 보호됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;둘째, 코드 확장이 용이합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;셋째, Mocking 원활해지고 테스트 코드 작성이 용이해집니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이렇게 얘기하면 잘 이해가 안갈 수 있으니 코드 예시로 보겠습니다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;이점1) 비즈니스 로직이 외부 호출과 명확히 분리되어 있어서, 코드 유지보수가 절감되며 도메인 로직이 보호됩니다.&lt;/strong&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;요구사항) Redis 조회가 너무 많이 발생하여 이로 인한 비용과 CPU 스로틀링이 크게 발생하고 있습니다. 이를 개선해주세요.&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드 예시&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;위 “&lt;strong&gt;#5. Driven Adapter (Output Port 의 구현체)”&lt;/strong&gt; 부분을 보면 Redis 와 외부 API 를 통해 배너 정보를 획득하고 있는 것을 볼 수 있었는데요.
Redis 부하 개선 요구사항을 받았다고 가정하고 로컬 캐시를 적용하는 형태로 코드를 개선해보겠습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// Redis 를 통해 정보를 조회하거나 저장하는 output port 구현체
&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;생략&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;

&lt;span&gt;// 외부 API 호출을 통해 정보를 조회하는 output port 구현체
&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;&lt;span&gt;생략&lt;/span&gt;&lt;span&gt;..&lt;/span&gt;

&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 신규 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 로컬 캐시에 저장된 정보를 조회하거나, 저장하는 output port 구현체
&lt;/span&gt;&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;@CacheConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cacheNames&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;"pdpBanners"&lt;/span&gt;&lt;span&gt;])&lt;/span&gt; &lt;span&gt;// 기본 캐시 이름 설정
&lt;/span&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerLocalCacheAdapter&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;

    &lt;span&gt;@Cacheable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;key&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;"#productId"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;// 로컬 캐시가 존재하지 않는 경우에 대한 처리. 캐시가 저장되지 않아야하므로 일반적으로는 null 응답. (또는 @Cacheable 을 사용하지 않고 CacheManager 를 사용하는 것도 가능)
&lt;/span&gt;    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;@CachePut&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;key&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;"#productId"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 변경 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 로컬 캐시 -&amp;gt; Redis 캐시 -&amp;gt; 외부 API 호출 순서로 값을 취하는 output port 구현체
&lt;/span&gt;&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerPrimaryAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerRedisAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;redisQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerRedisAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;redisCommandPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerExternalApiAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerLocalCacheAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;localCacheQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerLocalCacheAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;localCacheCommandPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;localCacheQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// 1. 로컬 캐시 조회 ----&amp;gt; 이게 추가되었고
&lt;/span&gt;            &lt;span&gt;?:&lt;/span&gt; &lt;span&gt;redisQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;also&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;// 2. Redis 조회
&lt;/span&gt;                  &lt;span&gt;localCacheCommandPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;it&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// Redis 데이터가 있다면 로컬 캐시에 저장
&lt;/span&gt;               &lt;span&gt;}&lt;/span&gt;
            &lt;span&gt;?:&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;also&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;// 3. API 호출 (로컬 &amp;amp; Redis 모두 없을 때)
&lt;/span&gt;                  &lt;span&gt;redisCommandPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;it&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// API 데이터를 Redis에 저장
&lt;/span&gt;                  &lt;span&gt;localCacheCommandPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;it&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// API 데이터를 로컬 캐시에 저장 ----&amp;gt; 이게 추가되었습니다.
&lt;/span&gt;               &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;위 코드를 보면 알 수 있듯이, 로컬 캐시를 사용하는 Port 하나가 추가되었고 Primary Adapter 로직에서 우선순위 로직만 변경된 것을 알 수 있습니다.
즉, 비즈니스 로직에서의 변경은 전혀 발생하지 않았습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이번에는 다른 예제를 보겠습니다. 다음과 같은 요구사항을 받았다고 가정합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;요구사항1) 과도한 MSA 로 인한 관리 피로도 및 비용 문제로 인해 일부 서비스를 모놀리식으로 재전환 하려합니다. 이로 인해 External API 대신에 RDB 를 직접 조회해주세요.&lt;/p&gt;
&lt;p&gt;요구사항2) 서버의 확장 전략을 Scale-out 보다는 Scale-up 하는 형태로 변경하려합니다. 레디스 캐시도 비용이니 호출 제거해주세요.&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드 예시&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// Redis 를 통해 정보를 조회하거나 저장하는 output port 구현체
&lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;코드&lt;/span&gt; &lt;span&gt;제거&lt;/span&gt;

&lt;span&gt;// 외부 API 호출을 통해 정보를 조회하는 output port 구현체
&lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;코드&lt;/span&gt; &lt;span&gt;제거&lt;/span&gt;

&lt;span&gt;// 로컬 캐시에 저장된 정보를 조회하거나, 저장하는 output port 구현체
&lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;유지&lt;/span&gt;&lt;span&gt;.&lt;/span&gt; &lt;span&gt;코드&lt;/span&gt; &lt;span&gt;생략&lt;/span&gt;

&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 신규 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// RDB 에 저장된 정보를 조회하는 output port 구현체
&lt;/span&gt;&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerMySqlAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;repository&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerJpaRepository&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;repository&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 변경 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 로컬 캐시 -&amp;gt; RDB 호출 순서로 값을 취하는 output port 구현체
&lt;/span&gt;&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerPrimaryAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerMySqlAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;mysqlQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerCaffeignAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;localCacheQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerCaffeignAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;localCacheCommandPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;localCacheQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// 로컬 캐시 조회
&lt;/span&gt;            &lt;span&gt;?:&lt;/span&gt; &lt;span&gt;mysqlQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;also&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;// RDB 조회 (로컬 캐시에 없을 때)
&lt;/span&gt;                  &lt;span&gt;localCacheCommandPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;it&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// 데이터를 로컬 캐시에 저장
&lt;/span&gt;            &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;위 코드를 보면 이번에도 마찬가지로 Output Port 에 대한 추가, 삭제만 있었을 뿐 비즈니스 로직 및 도메인 서비스의 변경 사항은 전혀 발생하지 않았습니다.
즉, 도메인 서비스에서는 항상 인터페이스인 Port 를 통해 데이터를 취하고 있기 때문에 외부 호출과 명확하게 분리되어있다고 할 수 있습니다. 이것은 코드 유지보수에도 상당히 긍정적인 영향을 미칩니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;이점2) 코드 확장이 용이합니다.&lt;/strong&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;요구사항) 배너 정보 외에 뱃지와 공지사항 정보를 추가해주세요.&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드 예시&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;앞서 Output 에서의 변경을 보신 것과는 달리, 이번에는 도메인 서비스로 한층 더 들어와서 코드 예시를 보겠습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 기존 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;ContentAggService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerPrimaryAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;
&lt;span&gt;):&lt;/span&gt; &lt;span&gt;ContentAggUseCase&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpBanners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toDomainObject&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 변경 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;ContentAggService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerPrimaryAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBadgePrimaryAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;badgePort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBadgeQueryPort&lt;/span&gt; &lt;span&gt;// Badge 정보 추가
&lt;/span&gt;    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpNoticePrimaryAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;noticePort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpNoticeQueryPort&lt;/span&gt; &lt;span&gt;// Notice 정보 추가
&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;ContentAggUseCase&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;badges&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;badgePort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// Badge 정보 조회
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;notices&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;noticePort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// Notice 정보 조회
&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
            &lt;span&gt;banners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toDomainObject&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;
            &lt;span&gt;badges&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;badges&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toDomainObject&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt; &lt;span&gt;// Badge 정보 포함
&lt;/span&gt;            &lt;span&gt;notices&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;notices&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toDomainObject&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;// Notice 정보 포함
&lt;/span&gt;        &lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;위 변경된 코드를 보면 알 수 있듯이, 하나의 Output Port 추가와 이를 Aggregate 하는 코드 몇줄만 추가되었습니다.
이로써 큰 변경 없이 비즈니스 로직을 구현할 수 있도록 도메인 객체가 만들어졌습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;요구사항) DTO 레벨에서 캐시하지 않고 Domain 레벨에서 캐시할 수 있도록 해주세요.&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드 예시&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이번에는 캐시 컨트롤에 대한 예제로도 한번 살펴보겠습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 기존 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;ContentAggService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerPrimaryAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBadgePrimaryAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;badgePort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBadgeQueryPort&lt;/span&gt;
&lt;span&gt;):&lt;/span&gt; &lt;span&gt;ContentAggUseCase&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;badges&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;badgePort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
            &lt;span&gt;pdpBanners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toDomainObject&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;
            &lt;span&gt;pdpBadges&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;badges&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toDomainObject&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;// Badge 정보 포함
&lt;/span&gt;        &lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 변경 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;ContentAggQueryPort&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;QueryPort&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;ContentAgg&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;// Content 애그리거트 조회 Output Port
&lt;/span&gt;
&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;ContentAggCommandPort&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;CommandPort&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;ContentAgg&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;// Content 애그리거트 저장 Output Port
&lt;/span&gt;
&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;ContentAggService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerPrimaryAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;contentAggQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ContentAggQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;contentAggCommandPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ContentAggCommandPort&lt;/span&gt;
&lt;span&gt;):&lt;/span&gt; &lt;span&gt;ContentAggUseCase&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;// 캐시에 데이터가 존재한다면 그대로 사용
&lt;/span&gt;        &lt;span&gt;contentAggQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;let&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;return&lt;/span&gt; &lt;span&gt;it&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;

        &lt;span&gt;// 캐시가 존재하지 않으면 데이터 조회
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;bannerPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;badges&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;badgePort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;contentAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ContentAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
            &lt;span&gt;pdpBanners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toDomainObject&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;
            &lt;span&gt;pdpBadges&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;badges&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toDomainObject&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;
        &lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// 조회된 데이터를 저장하여 캐싱
&lt;/span&gt;        &lt;span&gt;contentAggCommandPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;contentAgg&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;contentAgg&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;이 역시 앞선 예제와 마찬가지로 간단한 Output Port 추가만으로 요구사항 변경이 가능하다는 것을 확인할 수 있습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이번에는 쭉 앞쪽으로 이동해서 Input Port 쪽으로 와보겠습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;요구사항) PDP 선물하기 기능이 도입되었어요. 이로 인해서 PDP 에 노출하려는 컨텐츠가 기존과 많이 달라졌어요. 선물하기 PDP 를 구현해주세요.&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드 예시&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 기존 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;PdpPagePort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;generate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPage&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpPageAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pageUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpPageUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;LineMarginUseCase&lt;/span&gt;
&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPagePort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;generate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;coroutineScope&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;// Step 1. 애그리거트(Aggregate) 루트 객체 생성
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pdpAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPdpAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;// Step 2. Server-Driven UI 컴포넌트 생성 및 배치
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;page&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pageUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generatePage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAgg&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;// Step 3. 생성된 UI 컴포넌트들을 기준으로 컴포넌트 간 라인과 마진을 적용한다.
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;prettyPage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;applyLineAndMargin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;return&lt;/span&gt;&lt;span&gt;@coroutineScope&lt;/span&gt; &lt;span&gt;prettyPage&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 변경 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;PdpPagePort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;generate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAgg&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpAgg&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPage&lt;/span&gt;
    &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;supports&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pageType&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;String&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;Boolean&lt;/span&gt; &lt;span&gt;// 추가된 메서드
&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 반복되는 로직 분리
&lt;/span&gt;&lt;span&gt;abstract&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;BasePdpPageAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;LineMarginUseCase&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpPagePort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;generatePage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;coroutineScope&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;// Step 1. 애그리거트(Aggregate) 루트 객체 생성
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pdpAgg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPdpAgg&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;// Step 2. Server-Driven UI 컴포넌트 생성 및 배치
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;page&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;generate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAgg&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;// Step 3. 생성된 UI 컴포넌트들을 기준으로 컴포넌트 간 라인과 마진을 적용한다.
&lt;/span&gt;        &lt;span&gt;return&lt;/span&gt;&lt;span&gt;@coroutineScope&lt;/span&gt; &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;applyLineAndMargin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 기존 PDP 에 대한 대응
&lt;/span&gt;&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;DefaultPdpPageAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pageUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpPageUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;LineMarginUseCase&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;BasePdpPageAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;supports&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pageType&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;String&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;Boolean&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pageType&lt;/span&gt; &lt;span&gt;==&lt;/span&gt; &lt;span&gt;"default"&lt;/span&gt;

    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;generate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAgg&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpAgg&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pageUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generatePage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAgg&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 요구사항으로 받은 선물하기 PDP 에 대한 대응
&lt;/span&gt;&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;GiftPdpPageAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pageUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpPageUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;LineMarginUseCase&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;BasePdpPageAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAggUseCase&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;lineMarginUseCase&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;supports&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pageType&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;String&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;Boolean&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pageType&lt;/span&gt; &lt;span&gt;==&lt;/span&gt; &lt;span&gt;"gift"&lt;/span&gt;

    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;suspend&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;generate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAgg&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpAgg&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pageUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generateGiftPage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pdpAgg&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;@RestController&lt;/span&gt;
&lt;span&gt;@RequestMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"/pdp"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpController&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;pagePorts&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpPagePort&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;@GetMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"/{productId}"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getPdpPage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
        &lt;span&gt;@PathVariable&lt;/span&gt; &lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;@RequestParam&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;defaultValue&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;"default"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;pageType&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;String&lt;/span&gt; &lt;span&gt;// 파라미터 추가
&lt;/span&gt;    &lt;span&gt;):&lt;/span&gt; &lt;span&gt;PdpPage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;runBlocking&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;generator&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pagePorts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;it&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;supports&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pageType&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
            &lt;span&gt;?:&lt;/span&gt; &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;IllegalArgumentException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Unsupported page type: &lt;/span&gt;&lt;span&gt;$pageType&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;generator&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;BasePdpPageAdapter&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;generatePage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;PdpPagePort 를 인터페이스로 유지하여 PDP 유형별 다중 구현이 가능하도록 처리하였고 abstract class 를 추가하여 공통 로직을 분리하였습니다.
추후에 비슷한 요구사항을 받았을 때 class GiftPdpPageAdapter 와 같이 하나의 Port 구현체 추가만으로도 Controller, Port 에 대한 변경 없이 대응이 가능해졌습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이로써 Controller → Input Port → Domain → Output Port 흐름에 있어 각 모든 단계마다 코드의 큰 수정 없이 &amp;amp; 비즈니스 로직 변경 없이 요구사항이 반영이 가능하다는 것을 확인하였습니다.&lt;/p&gt;
&lt;p&gt;그런데 코드들을 보면서 느낀 점이 있으신가요? 각 계층은 모두 Port 와 Use Case 라는 인터페이스로만 통신한다는 점을 알 수 있는데요. 이것은 헥사고날 아키텍처의 가장 큰 특징이며, 이로인한 Mocking 이 원활할 수 있겠다라는 것을 직감적으로 느낄 수 있습니다.&lt;/p&gt;
&lt;p&gt;다음 예시에서 테스트가 용이해진 장점에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;이점3) Mocking 원활해지고 테스트 코드 작성이 용이해집니다.&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;첫번째 예시로 DB 조회를 통해 배너 정보를 가져오는 부분에 대해 테스트 코드를 작성해보겠습니다.&lt;/p&gt;
&lt;p&gt;다음 코드는 JPA 관련 테스트 코드를 레이어드 아키텍처와 헥사고날 아키텍처에서 서로 비교해본 예시입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;요구사항) RDB 를 사용하는 외부 호출부에 테스트 코드를 작성해주세요. (레이어드 아키텍처 vs. 헥사고날 아키텍처 비교)&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;레이어드 아키텍처 테스트 코드 예시&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// 레이어드 아키텍처 - JPA Repository 직접 사용
&lt;/span&gt;&lt;span&gt;@Service&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;ProductService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;productRepository&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ProductRepository&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getProduct&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;Product&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;productRepository&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;orElseThrow&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;RuntimeException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Product not found"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// JPA Repository (영속성 계층 포함)
&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;ProductRepository&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;JpaRepository&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Product&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;

&lt;span&gt;// 레이어드 아키텍처 테스트 코드
&lt;/span&gt;&lt;span&gt;@SpringBootTest&lt;/span&gt;
&lt;span&gt;@ExtendWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SpringExtension&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;ProductServiceTest&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;@Autowired&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;lateinit&lt;/span&gt; &lt;span&gt;var&lt;/span&gt; &lt;span&gt;productService&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ProductService&lt;/span&gt;

    &lt;span&gt;@MockBean&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;lateinit&lt;/span&gt; &lt;span&gt;var&lt;/span&gt; &lt;span&gt;productRepository&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ProductRepository&lt;/span&gt;

    &lt;span&gt;@Test&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;`상품 조회 - 존재하는 상품`&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;product&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;Product&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;1L&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;name&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;"Test Product"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;given&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productRepository&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1L&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;willReturn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Optional&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;of&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;product&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;

        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;result&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;productService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getProduct&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1L&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;assertEquals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Test Product"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;위 코드를 보면 ProductRepository 가 @Repository 로 동작하며, JPA 를 사용해야 테스트가 가능한 것을 볼 수 있습니다. 즉, JPA 관련 불필요한 설정과 의존성이 테스트에 남아있게 됩니다. Mocking 은 가능하지만 복잡한 설정이 필요하게 됩니다.
ProductRepository 인터페이스를 직접 Mocking 하고는 있지만, 향후 변경이 발생하면 MockBean 이 설정이 추가로 필요하게 됩니다.
이것이 바로 우리가 PR 이 올라오면 변경점들이 많게 느껴지는 이유 중 하나입니다.
테스트 속도를 저하시키는 @SpringBootTest 를 사용하게 되는 것도 JPA Repository 사용을 위해 JPA 설정을 로드하고 Repository 를 정상 주입하기 위해서입니다.
하지만 헥사고날 아키텍처는 Output Port 에 대해서만 Mocking 하면 되므로 JPA 나 복잡한 설정 없이 테스트가 가능합니다.&lt;/p&gt;
&lt;p&gt;다음 헥사고날 아키텍처 테스트 코드를 보겠습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;헥사고날 아키텍처 테스트 코드 예시&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// 헥사고날 아키텍처 - Port (Interface) 사용
&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;ProductPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;Product&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// UseCase - Repository 대신 Port 사용
&lt;/span&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;ProductUseCase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;productPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ProductPort&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;getProduct&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;Product&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;productPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;?:&lt;/span&gt; &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;RuntimeException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Product not found"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 헥사고날 아키텍처 테스트 코드
&lt;/span&gt;&lt;span&gt;@ExtendWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MockitoExtension&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;ProductUseCaseTest&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;@Mock&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;lateinit&lt;/span&gt; &lt;span&gt;var&lt;/span&gt; &lt;span&gt;productPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ProductPort&lt;/span&gt;

    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;lateinit&lt;/span&gt; &lt;span&gt;var&lt;/span&gt; &lt;span&gt;productUseCase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ProductUseCase&lt;/span&gt;

    &lt;span&gt;@BeforeEach&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;setUp&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;productUseCase&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ProductUseCase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productPort&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;@Test&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;`상품 조회 - 존재하는 상품`&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;product&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;Product&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;1L&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;name&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;"Test Product"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;`when`&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1L&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;thenReturn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;product&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;result&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;productUseCase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getProduct&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1L&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;assertEquals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Test Product"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;위 코드를 보면 알 수 있듯이 JPA 설정이 불필요하여 테스트가 단순해졌습니다.
@SpringBootTest, @Autowired가 불필요해졌기 때문에 테스트 속도가 빨라졌습니다.
또한 @Mock 을 사용하여 순수한 단위 테스트(Unit Test) 가 가능해졌습니다.
실행 속도 측면에서나 테스트 코드 작성 및 유지보수 측면에서에서 보나 모두 후자가 유리한 점을 알 수 있습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이번에는 기술 스택 변경에 따른 유연성을 검증해보겠습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;요구사항) DB, Storage 엔진 변경 등 기술 스택이 변경될 수도 있을텐데, 이를 테스트 코드 관점에서 유연성을 보여주세요.&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드 예시&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 기존 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerRedisAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;redisTemplate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;RedisTemplate&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;)&lt;/span&gt;
    &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;redisTemplate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;opsForValue&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdp-banner:&lt;/span&gt;&lt;span&gt;$productId&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;redisTemplate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;opsForValue&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdp-banner:&lt;/span&gt;&lt;span&gt;$productId&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerPrimaryAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerRedisAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;redisQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerRedisAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;redisCommandPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerApiAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;redisQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
            &lt;span&gt;?:&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;also&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
                &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isNotEmpty&lt;/span&gt;&lt;span&gt;())&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
                    &lt;span&gt;redisCommandPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;it&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
                &lt;span&gt;}&lt;/span&gt;
            &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 신규 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerValkeyAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;valkeyClient&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ValkeyClient&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;valkeyClient&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span&gt;"pdp-banner:&lt;/span&gt;&lt;span&gt;$productId&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;valkeyClient&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdp-banner:&lt;/span&gt;&lt;span&gt;$productId&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;@Component&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerPrimaryAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerValkeyAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;valkeyQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 주입되는 어댑터만 redis -&amp;gt; valkey 로 변경되었습니다.
&lt;/span&gt;    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerValkeyAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;valkeyCommandPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;@Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"pdpBannerApiAdapter"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;private&lt;/span&gt; &lt;span&gt;val&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;override&lt;/span&gt; &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Long&lt;/span&gt;&lt;span&gt;):&lt;/span&gt; &lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;&amp;gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;valkeyQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
            &lt;span&gt;?:&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;also&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
                &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isNotEmpty&lt;/span&gt;&lt;span&gt;())&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
                    &lt;span&gt;valkeyCommandPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;it&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
                &lt;span&gt;}&lt;/span&gt;
            &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;// 테스트 코드
&lt;/span&gt;&lt;span&gt;////////////
&lt;/span&gt;&lt;span&gt;@ExtendWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MockitoExtension&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;PdpBannerPrimaryAdapterTest&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;@Mock&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;lateinit&lt;/span&gt; &lt;span&gt;var&lt;/span&gt; &lt;span&gt;globalCacheQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;  &lt;span&gt;// 글로벌 캐시 조회 포트 (Redis, Valkey 등 사용 가능)
&lt;/span&gt;
    &lt;span&gt;@Mock&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;lateinit&lt;/span&gt; &lt;span&gt;var&lt;/span&gt; &lt;span&gt;globalCacheCommandPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerCommandPort&lt;/span&gt;  &lt;span&gt;// 글로벌 캐시 저장 포트
&lt;/span&gt;
    &lt;span&gt;@Mock&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;lateinit&lt;/span&gt; &lt;span&gt;var&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerQueryPort&lt;/span&gt;  &lt;span&gt;// 외부 API 조회 포트
&lt;/span&gt;
    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;lateinit&lt;/span&gt; &lt;span&gt;var&lt;/span&gt; &lt;span&gt;primaryAdapter&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;PdpBannerPrimaryAdapter&lt;/span&gt;

    &lt;span&gt;@BeforeEach&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;setUp&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;primaryAdapter&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;PdpBannerPrimaryAdapter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;globalCacheQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;globalCacheCommandPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;@Test&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;`글로벌 캐시에 데이터가 있으면 API를 호출하지 않고 반환한다`&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;// Given
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;productId&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;100L&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;listOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;1L&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;title&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;"Banner 1"&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;

        &lt;span&gt;`when`&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;globalCacheQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;thenReturn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;banners&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// When
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;result&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;primaryAdapter&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Then
&lt;/span&gt;        &lt;span&gt;assertNotNull&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;assertEquals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;result&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;assertEquals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Banner 1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;result&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;first&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Verify (API 호출이 발생하지 않아야 함)
&lt;/span&gt;        &lt;span&gt;verify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;()).&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;

        &lt;span&gt;// Verify (글로벌 캐시에 데이터가 있기 때문에 저장 로직이 실행되지 않아야 함)
&lt;/span&gt;        &lt;span&gt;verify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;globalCacheCommandPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;()).&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;@Test&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;`글로벌 캐시에 데이터가 없고 API에서 가져오면 글로벌 캐시에 저장 후 반환한다`&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;// Given
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;productId&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;200L&lt;/span&gt;
        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;listOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;PdpBanner&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;2L&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;title&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;"Banner 2"&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;

        &lt;span&gt;`when`&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;globalCacheQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;thenReturn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;  &lt;span&gt;// 글로벌 캐시에 데이터 없음
&lt;/span&gt;        &lt;span&gt;`when`&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;thenReturn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;banners&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;  &lt;span&gt;// API에서 데이터 조회
&lt;/span&gt;
        &lt;span&gt;// When
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;result&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;primaryAdapter&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Then
&lt;/span&gt;        &lt;span&gt;assertNotNull&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;assertEquals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;result&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;assertEquals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Banner 2"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;result&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;first&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Verify (API가 호출되었는지 확인)
&lt;/span&gt;        &lt;span&gt;verify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Verify (API에서 조회한 데이터를 글로벌 캐시에 저장했는지 확인)
&lt;/span&gt;        &lt;span&gt;verify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;globalCacheCommandPort&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;banners&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;@Test&lt;/span&gt;
    &lt;span&gt;fun&lt;/span&gt; &lt;span&gt;`글로벌 캐시와 API 모두에 데이터가 없으면 null을 반환한다`&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;// Given
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;productId&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;300L&lt;/span&gt;
        &lt;span&gt;`when`&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;globalCacheQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;thenReturn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;  &lt;span&gt;// 글로벌 캐시에 데이터 없음
&lt;/span&gt;        &lt;span&gt;`when`&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;thenReturn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;  &lt;span&gt;// API에도 데이터 없음
&lt;/span&gt;
        &lt;span&gt;// When
&lt;/span&gt;        &lt;span&gt;val&lt;/span&gt; &lt;span&gt;result&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;primaryAdapter&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Then
&lt;/span&gt;        &lt;span&gt;assertNull&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Verify (API 호출이 발생했는지 확인)
&lt;/span&gt;        &lt;span&gt;verify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;apiQueryPort&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;findByProductId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;productId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;// Verify (글로벌 캐시 저장이 수행되지 않아야 함)
&lt;/span&gt;        &lt;span&gt;verify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;globalCacheCommandPort&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;()).&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;테스트 코드를 보면 알 수 있듯이 특정 캐시 기술에 의존하지 않고 헥사고날 아키텍처의 인터페이스만을 활용하여 테스트가 수행된 것을 알 수 있습니다.
즉, Redis → Valkey 로 기술 스택이 변경된다고 할지라도 테스트 코드는 변경없이 유지되며 올바른 검증을 수행합니다.
이는 위 사례로 든 Output Port 에 대한 검증 뿐 아니라, 도메인 로직을 구현하는 서비스 코드에서도 마찬가지로 기술 스택 변경에 따른 영향을 받지 않습니다.&lt;/p&gt;&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;이 외에도 Mocking 이 원활해짐으로 인해 얻는 장점들이 많습니다. 추가적인 예시를 들면 다음과 같습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;API 개발이 빨라집니다.
&lt;ol&gt;
&lt;li&gt;내가 만드는 API 작업에 있어서 mock 을 우선 제공하려할때 용이합니다.&lt;/li&gt;
&lt;li&gt;타팀 디펜던시를 받고 있는 경우 우선 mock 처리하여 나의 개발 진행이 용이합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;JVM 웜업 전용 API 만들기에도 용이합니다.
&lt;ol&gt;
&lt;li&gt;레디스, RDB, 외부 API 호출 등 모든 Output Port 영역은 Mock 처리하고 그 외 모든 클래스의 웜업에 집중할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Output Port 전용 웜업을 구현하기 용이합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;단위 테스트 작성이 빠르다보니 테스트 커버리지를 올리기에 용이합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;이렇게 해서 Domain-Driven 헥사고날 아키텍처를 코드 예시를 통해 알아보는 시간을 가졌습니다.&lt;/p&gt;
&lt;p&gt;이 아키텍처의 목표를 요약하자면 이렇습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Port 를 이용한 패턴으로 비즈니스 로직 및 도메인 로직을 외부의 입출력과 명확하게 분리한다.&lt;/li&gt;
&lt;li&gt;비즈니스 요구사항을 해결하기 위해 단순히 Service 클래스로만 구성하는 것이 아니라 Use Case 인터페이스 방식을 활용한다.&lt;/li&gt;
&lt;li&gt;DDD(Domain-Driven Design) 와 헥사고날 아키텍처는 서로 보완 관계에 있다. DDD 를 이용하여 도메인 중심적 설계를 하고, 헥사고날 아키텍처는 도메인 모델이 효과적으로 사용될 수 있도록 구조를 제공한다.&lt;/li&gt;
&lt;li&gt;도메인 모델을 구성하는 데에 어느정도 복잡도가 있는 경우, 애그리거트 모델을 이용하여 설계할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;다음 시간에 기회가 된다면 앞서 소개한 장점과 대비될 수 있는 헥사고날 아키텍처를 사용할때 발생할 수 있는 단점이나 트레이드 오프에 대해 사례를 들어 나열해보고, 이를 어떻게 개선했는지에 대해서도 소개하는 시간을 가져보겠습니다.&lt;/p&gt;
&lt;p&gt;감사합니다. 🙂&lt;/p&gt;
&lt;/div&gt;
</ns0:encoded></item></channel></rss>